function [gameTree, gameList] = configureDotris
%Configure a familiar falling blocks dotris with Snow Dots
%
%   [gameTree, gameList] = configureDotris
%
%   gameTree is a topsTreeNode object which organizes the dotris.
%   gameTree.run() will start the dotris.  gameTree.gui() will launch a
%   graphical interface for viewing the organization of the dotris.
%
%   gameList is a topsGroupedList object which holds all the objects and
%   data needed to run the dotris, including gameTree.  gameList.gui() will
%   launch a graphical interface for viewing the objects and data.
%
%   The dotris is similar to the popular Tetris video dotris.  The player
%   uses the gamepad or keyboard to move and rotate falling blocks, in an
%   effort to form complete rows of blocks.
%
%   The dotris uses a dotsQueryableHIDGamepad for input.  It uses
%   dotsDrawableTargets to display the dotris board and blocks.  It uses
%   topsTheData log to keep track of what the player did.
%
%   Some dotris parameters, such as the size of the dotris board, can be edited
%   near the top of this file.
%
%   The dotris is implemented with "ideas".  An "idea" is nothing magical,
%   just a string plus an organizational tactic.  The string is added to
%   gameList in the "ideas" group and also reused in multiple places during
%   configuration as a local variable.
%
%   An idea string such as 'drop' might be reused as the name of a
%   topsStateMachine state and as a classification name for input data that
%   are related to the state.  The "idea" tactic makes explicit the
%   relationship the state and the data classification.  It also makes it
%   possible to compare states and classificaitons just by comparing
%   strings.  This reduces the need for extra comparison code.
%
% 2010 benjamin.heasly@gmail.com
%   Seattle, WA

%% Organization:
% a container for all dotris data and objects
%   partitioned into arbitrary groups
%   viewable with gameList.gui
gameList = topsGroupedList;

%% Constants:
% store some constants in the gameList container
%   used during configuration
%   used while dotris is running
gameList{'control'}{'game time'} = inf;
gameList{'control'}{'piece fall interval'} = 1;
gameList{'control'}{'board height'} = 24;
gameList{'control'}{'board width'} = 12;
gameList{'control'}{'slides per second'} = 5;

gameList{'input'}{'gamepad dead zone'} = .01;

gameList{'graphics'}{'full screen'} = true;
gameList{'graphics'}{'degrees per square'} = .5;
gameList{'graphics'}{'square pixels'} = 10;
gameList{'graphics'}{'border color'} = [32 32 192];
gameList{'graphics'}{'pile color'} = [64 64 64];
gameList{'graphics'}{'piece color'} = [128 32 0];
gameList{'graphics'}{'next piece color'} = [192 0 32];

scoreOffset.x = 0;
scoreOffset.y = -1 * gameList{'graphics'}{'degrees per square'};
gameList{'graphics'}{'score offset'} = scoreOffset;

nextOffset.x = 0;
nextOffset.y = 2 * gameList{'graphics'}{'degrees per square'};
gameList{'graphics'}{'next piece offset'} = nextOffset;

%% Ideas:
% jot down some reusable idea strings
%   they can be anything--any concept or string to reuse
%   save them in the gameList and keep them around as local variables
dotris = gameList.addIdea('dotris');
ready = gameList.addIdea('ready');
launch = gameList.addIdea('launch');
land = gameList.addIdea('land');
fall = gameList.addIdea('fall');
drop = gameList.addIdea('drop');
ccw = gameList.addIdea('counterclockwise');
cw = gameList.addIdea('clockwise');
left = gameList.addIdea('left');
right = gameList.addIdea('right');
pause = gameList.addIdea('pause');
noInput = gameList.addIdea('noInput');
gameOver = gameList.addIdea('gameOver');

%% Graphics:
% create some graphics objects
%   configure them using constants from above
%   store them in the gameList container
dotsTheDrawablesManager.reset;
dm = dotsTheDrawablesManager.theObject;

border = dm.newObject('dotsDrawableTargets');
dm.addObjectToGroup(border, dotris);
border.color = gameList{'graphics'}{'border color'};
gameList{'graphics objects'}{'border'} = border;

piece = dm.newObject('dotsDrawableTargets');
dm.addObjectToGroup(piece, dotris);
piece.color = gameList{'graphics'}{'piece color'};
gameList{'graphics objects'}{'piece'} = piece;

nextPiece = dm.newObject('dotsDrawableTargets');
dm.addObjectToGroup(nextPiece, dotris);
nextPiece.color = gameList{'graphics'}{'next piece color'};
gameList{'graphics objects'}{'next piece'} = nextPiece;

pile = dm.newObject('dotsDrawableTargets');
dm.addObjectToGroup(pile, dotris);
pile.color = gameList{'graphics'}{'pile color'};
gameList{'graphics objects'}{'pile'} = pile;

dm.setGroupProperty(dotris, 'size', gameList{'graphics'}{'square pixels'});
dm.setGroupProperty(dotris, 'shape', 0);

score = dm.newObject('dotsDrawableText');
dm.addObjectToGroup(score, dotris);
score.color = gameList{'graphics'}{'border color'};
gameList{'graphics objects'}{'score'} = score;

dm.activateGroup(dotris);

%% Input:
% create an input object, a gamepad
%   configure it to classify inputs as needed for this dotris
%   store it in the gameList container
qm = dotsTheQueryablesManager.theObject;
gp = qm.newObject('dotsQueryableHIDGamepad');
gameList{'input objects'}{'gamepad'} = gp;
if gp.isAvailable
    in = gp;

    % identify axis and button press phenomenons
    %   see gp.phenomenons.gui()
    downInput = gp.phenomenons{'axes'}{'max_Y'};
    dropInput = gp.phenomenons{'pressed'}{'pressed_button_1'};
    ccwInput = gp.phenomenons{'pressed'}{'pressed_button_2'};
    cwInput = gp.phenomenons{'pressed'}{'pressed_button_3'};
    pauseInput = gp.phenomenons{'pressed'}{'pressed_button_4'};
    
    % make a button combo for dropping a piece
    dropInput = dotsPhenomenon.composite( ...
        [downInput, dropInput], 'intersection');

else
    kb = qm.newObject('dotsQueryableHIDKeyboard');
    gameList{'input objects'}{'keyboard'} = kb;
    in = kb;
    
    kb.keysOfInterest = {'KeyboardSpacebar', 'KeyboardF', 'KeyboardJ', ...
        'KeyboardR', 'KeyboardI', 'KeyboardP'};
    kb.initialize;
    
    dropInput = kb.phenomenons{'pressed'}{'pressed_KeyboardSpacebar'};
    ccwInput = kb.phenomenons{'pressed'}{'pressed_KeyboardJ'};
    cwInput = kb.phenomenons{'pressed'}{'pressed_KeyboardI'};
    pauseInput = kb.phenomenons{'pressed'}{'pressed_KeyboardP'};
    
    leftInput = kb.phenomenons{'pressed'}{'pressed_KeyboardR'};
    rightInput = kb.phenomenons{'pressed'}{'pressed_KeyboardF'};

    kb.addClassificationInGroupWithRank(leftInput,  left,	dotris, 102);
    kb.addClassificationInGroupWithRank(rightInput,	right,	dotris,	103);
end
gameList{'input objects'}{'using'} = in;
in.unavailableOutput = noInput;

% classify input data
%   by matching input phenomenons with "ideas" from above
in.addClassificationInGroupWithRank(dropInput,  drop,	dotris, 1);
in.addClassificationInGroupWithRank(ccwInput,   ccw,	dotris,	100);
in.addClassificationInGroupWithRank(cwInput,    cw,     dotris,	101);
in.addClassificationInGroupWithRank(pauseInput, pause,	dotris,	1000);
in.activeClassificationGroup = dotris;

% input behavior depends on paused/unpaused state
in.addClassificationInGroupWithRank(pauseInput, ready,	pause,	1);

%% Control:
% create four types of control objects:
%	- dotrisLogic organizes logic for the game itself
%       (see dotrisLogic.m in the dotris/ folder)
%	- topsTreeNode organizes flow outside of gameplay
%   - topsStateMachine organizes flow during gameplay
%   - topsCallList organizes batches of functions to be called together
%   - topsConcurrentComposite interleaves behaviors of the state machine and call
%   list.
% connect these control objects to each other
% store them in the gameList container

% the logic engine of the dotris game
gameLogic = dotrisLogic;
gameLogic.tickIntervalDefault = gameList{'control'}{'piece fall interval'};
gameLogic.boardHeight = gameList{'control'}{'board height'};
gameLogic.boardWidth = gameList{'control'}{'board width'};
gameLogic.squareScale = gameList{'graphics'}{'degrees per square'};

% tell the logic engine to report events using "ideas" from above
gameLogic.outputTickTimeUp = fall;
gameLogic.outputTickOK = '';
gameLogic.outputRatchetLanded = land;
gameLogic.outputRatchetOK = '';
gameLogic.outputJudgeGameOver = gameOver;
gameLogic.outputJudgeOK = '';
gameList{'control objects'}{'game logic'} = gameLogic;

% let the logic engine call back to update graphics
%   gameLogic will pass in itself as the first argument
gameLogic.fevalablePieceChanged = {@updatePiece, piece, nextPiece, nextOffset};
gameLogic.fevalablePieceMoved = {@movePiece, piece};
gameLogic.fevalablePileChanged = {@updatePile, pile, score};

% pin the piece's horizontal sliding to the window frame rate
%   This allows the sliding to repeat periodically
%   without the slide *rate* depending on the frame rate
deadZone = gameList{'input'}{'gamepad dead zone'};
slidesPerSecond = gameList{'control'}{'slides per second'};
gameLogic.slideThreshold = dm.windowFrameRate / slidesPerSecond;

% the game calls bundles concurrent behaviors like
%	- drawing graphics
%   - getting user input
gameCalls = topsCallList;
gameCalls.alwaysRunning = true;
gameCalls.addCall({@readData, in});
gameCalls.addCall({@pieceMaySlide, gameLogic, gp, deadZone});
gameCalls.addCall({@mayDrawNextFrame, dm});
gameList{'control objects'}{'game calls'} = gameCalls;

% state machine to manage flow through events defined by dotrisLogic
gameMachine = topsStateMachine;
gameMachine.name = dotris;
gameList{'control objects'}{'game machine'} = gameMachine;

gameStates = { ...
    'name',         'next',     'input',            'entry',
    launch,         ready,      {},                 {@newPiece, gameLogic}; ...
    ready,          'may fall', {},                 {}; ...
    'may fall',     'may move', {@tick, gameLogic}, {}; ...
    'may move',     ready,      {@queryNextTracked, in},    {}; ...
    fall,           ready,      {@ratchet, gameLogic},      {}; ...
    noInput,        drop,       {},                 {}; ...
    ccw,            ready,      {},                 {@spinPiece, gameLogic, +90}; ...
    cw,             ready,      {},                 {@spinPiece, gameLogic, -90}; ...
    left,           ready,      {},                 {@slidePiece, gameLogic, -1}; ...
    right,          ready,      {},                 {@slidePiece, gameLogic, +1}; ...
    drop,           land,       {},                 {@dropPiece, gameLogic}; ...
    land,           'judgement',{},                 {@landPiece, gameLogic}; ...
    'judgement',	launch,     {@judge, gameLogic},{}; ...
    gameOver,       '',         {},                 {@logAllData, in}; ...
    };
gameMachine.addMultipleStates(gameStates);

% add a "pause" state, which uses some additional properties
pauseState.name = pause;
pauseState.next = pause;
pauseState.timeout = 1;
pauseState.input = {@queryNextTracked, in};
pauseState.entry = {@togglePaused, gameList, true};
pauseState.exit = {@togglePaused, gameList, false};
gameMachine.addState(pauseState);

% topsConcurrentComposite keeps the gameMachine and the gameCalls running concurrently
gameConcurrents = topsConcurrentComposite;
gameConcurrents.name = dotris;
gameConcurrents.startFevalable = {@startGame, gameList};
gameConcurrents.addChild(dm);
gameConcurrents.addChild(gameCalls);
gameConcurrents.addChild(gameMachine);

% the trunk of the gameTree sets up the screen
%	a branch of the gameTree to lauches gameplay
gameTree = topsTreeNode;
gameTree.name = dotris;
gameTree.startFevalable = {@openScreenWindow, dm};
gameTree.finishFevalable = {@closeScreenWindow, dm};
gameList{'control objects'}{'game tree'} = gameTree;

playTree = gameTree.newChildNode;
playTree.name = 'gameplay';
playTree.addChild(gameConcurrents);


%% Glue together some graphics, input, and control

% let dotsTheSwitchboard tell objects how to count time
%   See dotsTheMachineConfiguration for the local, default clockFunction
dotsTheSwitchboard.sharePropertyOfObject('clockFunction', gameMachine);
dotsTheSwitchboard.sharePropertyOfObject('clockFunction', gameLogic);
dotsTheSwitchboard.sharePropertyOfObject('clockFunction', topsDataLog.theDataLog);

% set up play area graphics
[x,y] = gameLogic.gridToScaledXY(gameLogic.boardGrid);
border.x = x;
border.y = y;
score.x = mean(x) + scoreOffset.x;
score.y = min(y) + scoreOffset.y;

%% Custom Behaviors:

% Start a fresh game
function startGame(gameList)
% initialize the game
gameLogic = gameList{'control objects'}{'game logic'};
gameLogic.startDotris;

% flush out stale gamepad or keyboard data
in = gameList{'input objects'}{'using'};
in.flushData;


% Start or stop in the middle of the game
function togglePaused(gameList, isPaused)
dm = dotsTheDrawablesManager.theObject;
in = gameList{'input objects'}{'using'};
if isPaused
    pause = gameList{'ideas'}{'pause'};
    dm.activateGroup(pause);
    in.activeClassificationGroup = pause;
else
    dotris = gameList{'ideas'}{'dotris'};
    dm.activateGroup(dotris);
    in.activeClassificationGroup = dotris;
end


% Report x axis to the game logic, which may move the piece
%   Called each frame as dotsTheDrawablesManager sets its lastFrameTime
function pieceMaySlide(gameLogic, gamepad, deadZone)
xVal = gamepad.xValue;
if ~isempty(xVal) && abs(xVal) > deadZone;
    gameLogic.incrementSlideCounter(xVal);
else
    gameLogic.slideCounter = 0;
end


% Update x and y positions for the piece graphics
function movePiece(gameLogic, piece)
[x,y] = gameLogic.gridToScaledXY(gameLogic.pieceGrid);
piece.x = x;
piece.y = y;


% Update the piece and nextPiece graphics
function updatePiece(gameLogic, piece, nextPiece, nextOffset)
[x,y] = gameLogic.gridToScaledXY(gameLogic.pieceGrid);
piece.x = x;
piece.y = y;

[x,y] = gameLogic.gridToScaledXY(gameLogic.nextPieceGrid);
nextPiece.x = x + nextOffset.x;
nextPiece.y = y + nextOffset.y;


% Update pile graphics, log piece and score data
function updatePile(gameLogic, pile, score)
[x,y] = gameLogic.gridToScaledXY(gameLogic.pileGrid);
pile.x = x;
pile.y = y;

score.string = sprintf('%d lines', gameLogic.score);

topsDataLog.logDataInGroup(gameLogic.score, 'score');
topsDataLog.logDataInGroup(gameLogic.pieceType, 'piece type');
topsDataLog.logDataInGroup(gameLogic.nextPieceType, 'next piece type');